Subobject同步优化

基于 PushModelSubobject 同步优化;

基本机制

UE 提供了 PushModel 机制:

对于 Property 这一层,通过 UEPushModelPrivate::MarkPropertyDirty 主动将其标记为 Dirty

FObjectReplicator::ReplicateProperties_r 进行属性同步时,通过 PushModelState->IsPropertyDirty 判断属性是否 Dirty

既然可以判断一个同步对象上的同步 Property 是否有变更,就可以筛选出没有属性变化的对象;可以在 UActorChannel::ReplicateActorUActorChannel::WriteSubObjectInBunch 时,判断 FObjectReplicator::CanSkipUpdate 跳过这些对象,加快同步速度;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
bCanSkip = true;

// Have the RepFlags changed ?
bCanSkip = bCanSkip && SendingRepState.RepFlags.Value == RepFlags.Value;

// Any Naks to handle ?
bCanSkip = bCanSkip && SendingRepState.NumNaks == 0;

// Have we compared the properties twice ?
bCanSkip = bCanSkip && SendingRepState.LastCompareIndex > 1;

// Do we have open Ack's ?
bCanSkip = bCanSkip && !(SendingRepState.bOpenAckedCalled && SendingRepState.PreOpenAckHistory.Num() > 0);

// Any changelists to send ?
bCanSkip = bCanSkip && SendingRepState.LastChangelistIndex == RepChangelistState.HistoryEnd;

// Is the changelist history fully acknowledged ?
bCanSkip = bCanSkip && SendingRepState.HistoryStart == SendingRepState.HistoryEnd;

// Are we resending data for replay ?
bCanSkip = bCanSkip && Connection->ResendAllDataState == EResendAllDataState::None;

// Are we forcing a compare property ?
bCanSkip = bCanSkip && !OwningChannel->bForceCompareProperties;

// Are any CustomDelta properties dirty ?
bCanSkip = bCanSkip && (RepChangelistState.CustomDeltaChangeIndex == SendingRepState.CustomDeltaChangeIndex && !SendingRepState.HasAnyPendingRetirements());

// Are any replicated properties dirty ?
bCanSkip = bCanSkip && RepChangelistState.HasAnyDirtyProperties() == false;

针对 Subobject 进一步优化

可以观察到 PushModelAActor::ReplicateSubobjects 时,内部针对每个 SubObject 时,进行了一系列优化(CanSkip 的判定);

但是还是每帧在遍历 ReplicatedComponents 进行判定,并且判定的逻辑本身比较复杂,常数较大,也会造成很大的开销;

这意味着即使没有任何 ComponentPropertyMarkDirty,本身 ReplicatedComponents 越多,开销也会随之越大;

基本思路

需要一个机制更加精确地进行 ReplicateSubobject,做到对于非 DirtyComponent 直接不遍历;

显然,对于一个 Actor,对于所有需要同步这个 ActorConnection,可以直接记录下 DirtyComponents,即发生了属性变化但还未同步更新的 Subobject;在每次 Dirty 时加入,在同步后移除;

NotifyDirty

首先需要可以知道一个 ComponentMarkDirty

PushModel 中新增 Delegate

1
2
3
4
5
6
7
class NETCORE_API FNetPushModelDelegate
{
public:

DECLARE_MULTICAST_DELEGATE_OneParam(FMarkPropertyDirty, const UEPushModelPrivate::FNetPushObjectId&);
static FOnMarkPropertyDirtyDelegate OnMarkPropertyDirty;
};

修改 PushModel::MarkPropertyDirty

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void MarkPropertyDirty(const FNetLegacyPushObjectId ObjectId, const int32 RepIndex)
{
const int32 ObjectIndex = ObjectId;
if (LIKELY(PerObjectStates.IsValidIndex(ObjectIndex)))
{
PerObjectStates[ObjectIndex].MarkPropertyDirty(static_cast<uint16>(RepIndex));
FNetPushModelDelegate::OnMarkPropertyDirty.Broadcast(ObjectIndex);
}
}

void MarkPropertyDirty(const FNetLegacyPushObjectId ObjectId, const int32 StartRepIndex, const int32 EndRepIndex)
{
const int32 ObjectIndex = ObjectId;
if (LIKELY(PerObjectStates.IsValidIndex(ObjectIndex)))
{
FPushModelPerObjectState& ObjectState = PerObjectStates[ObjectIndex];
for (int32 RepIndex = StartRepIndex; RepIndex <= EndRepIndex; ++RepIndex)
{
ObjectState.MarkPropertyDirty(static_cast<uint16>(RepIndex));
FNetPushModelDelegate::OnMarkPropertyDirty.Broadcast(ObjectIndex);
}
}
}

Connection

对于每个 Actor,维护一份 Data,其中维护对其可见的 Connection

显然可以在 AActor::ReplicateSubobjects 时,将 Connection 加入存储列表;

RemoveClientConnection 时,将 Connection 从所有的 ActorData 中移除;

DirtyComponents

在每次收到 FNetPushModelDelegate::OnMarkPropertyDirty 时,记录该 Subobject 到其 Parent 维护的 DirtyComponents 数据中;

同时,当 ActorComponent 进行 RegisterUnregister 时,也需要将其加入 DirtyComponents;

特别地,为了考虑丢包的情况,判断取出 FSendingRepState* SendingRepState = Channel->ActorReplicator->RepState->GetSendingRepState(),然后判定 SendingRepState->NumNaks > 0 时(即存在丢包时),将所有数据重新传一遍(即所有的 ReplicatedComponents 都加入 DirtyComponents);

每次 ReplicateSubobjects 后,清空 DirtyComponents

这样就可以保证 DirtyComponents 的个数限制,未 MarkDirty 的不参与 Replicate